/*
 * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.truffle.dsl.processor.library;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import com.oracle.truffle.dsl.processor.ProcessorContext;
import com.oracle.truffle.dsl.processor.java.ElementUtils;
import com.oracle.truffle.dsl.processor.java.compiler.CompilerFactory;
import com.oracle.truffle.dsl.processor.parser.AbstractParser;

public class LibraryParser extends AbstractParser<LibraryData> {

    @Override
    public boolean isDelegateToRootDeclaredType() {
        return true;
    }

    @Override
    protected LibraryData parse(Element element, List<AnnotationMirror> mirrors) {
        TypeElement type = (TypeElement) element;
        if (mirrors.isEmpty()) {
            return null;
        }
        AnnotationMirror mirror = mirrors.iterator().next();

        LibraryData model = new LibraryData((TypeElement) element, mirror);

        if (!ElementUtils.typeEquals(type.getSuperclass(), types.Library)) {
            model.addError("Declared library classes must exactly extend the type %s.", ElementUtils.getQualifiedName(types.Library));
            return model;
        }

        if (!element.getModifiers().contains(Modifier.ABSTRACT)) {
            model.addError("Declared library classes must be abstract.");
            return model;
        }
        if (element.getEnclosingElement().getKind() != ElementKind.PACKAGE && !element.getModifiers().contains(Modifier.STATIC)) {
            model.addError("Declared inner library classes must be static.");
            return model;
        }

        AnnotationMirror generateAOT = ElementUtils.findAnnotationMirror(element, types.GenerateAOT);
        if (generateAOT != null) {
            model.setGenerateAOT(true);
        }

        AnnotationMirror invalidAnnotation = ElementUtils.findAnnotationMirror(element, types.GenerateCached);
        if (invalidAnnotation == null) {
            invalidAnnotation = ElementUtils.findAnnotationMirror(element, types.GenerateUncached);
        }
        if (invalidAnnotation == null) {
            invalidAnnotation = ElementUtils.findAnnotationMirror(element, types.GenerateInline);
        }
        if (invalidAnnotation != null) {
            model.addError(invalidAnnotation, null, "The annotation @%s cannot be used on a library.", ElementUtils.getSimpleName(invalidAnnotation.getAnnotationType()));
            return model;
        }

        model.setDefaultExportLookupEnabled(ElementUtils.getAnnotationValue(Boolean.class, mirror, "defaultExportLookupEnabled"));
        model.setDynamicDispatchEnabled(ElementUtils.getAnnotationValue(Boolean.class, mirror, "dynamicDispatchEnabled"));
        model.setPushEncapsulatingNode(ElementUtils.getAnnotationValue(Boolean.class, mirror, "pushEncapsulatingNode"));

        boolean defaultExportReachable = true;
        List<AnnotationMirror> defaultExports = ElementUtils.getRepeatedAnnotation(element.getAnnotationMirrors(), types.GenerateLibrary_DefaultExport);
        for (AnnotationMirror defaultExport : defaultExports) {
            LibraryDefaultExportData export = loadDefaultExportImpl(model, defaultExport, "value");
            if (export == null) {
                continue;
            }

            for (LibraryDefaultExportData prev : model.getDefaultExports()) {
                if (ElementUtils.isAssignable(export.getReceiverType(), prev.getReceiverType())) {
                    model.addError(defaultExport, null, "The receiver type '%s' of the export '%s' is not reachable. " +
                                    "It is shadowed by receiver type '%s' of export '%s'.",
                                    ElementUtils.getSimpleName(export.getReceiverType()),
                                    ElementUtils.getSimpleName(export.getImplType()),
                                    ElementUtils.getSimpleName(prev.getReceiverType()),
                                    ElementUtils.getSimpleName(prev.getImplType()));
                    break;
                }
            }

            model.getDefaultExports().add(export);
            if (ElementUtils.isAssignable(context.getType(Object.class), export.getReceiverType())) {
                defaultExportReachable = false;
            }
        }

        parseAssertions(element, mirror, type, model);

        List<ExecutableElement> allMethods = ElementFilter.methodsIn(CompilerFactory.getCompiler(type).getEnclosedElementsInDeclarationOrder(type));
        allMethods.add(ElementUtils.findExecutableElement(types.Library, "accepts"));

        TypeMirror inferredReceiverType = null;

        Map<String, List<LibraryMessage>> messageByName = new LinkedHashMap<>();
        for (ExecutableElement executable : allMethods) {
            Modifier visibility = ElementUtils.getVisibility(executable.getModifiers());
            if (visibility == Modifier.PRIVATE) {
                continue;
            } else if (executable.getModifiers().contains(Modifier.FINAL)) {
                continue;
            } else if (executable.getModifiers().contains(Modifier.STATIC)) {
                continue;
            } else if (model.isDynamicDispatch() && executable.getSimpleName().toString().equals("cast")) {
                // the cast method is abstract but ignore in the dynamic dispatch library.
                // it is automatically implemented.
                continue;
            }
            boolean isDeprecated = ElementUtils.isDeprecated(executable);
            String messageName = executable.getSimpleName().toString();

            LibraryMessage message = new LibraryMessage(model, messageName, executable, isDeprecated);
            messageByName.computeIfAbsent(messageName, (e) -> new ArrayList<>()).add(message);

            if (visibility == null) {
                message.addError("Library messages must be public or protected. Annotate with @GenerateLibrary.Ignore to ignore this method for generation. ");
            }

            if (executable.getParameters().isEmpty()) {
                message.addError("Not enough arguments specified for a library message. " +
                                "The first argument of a library method must be of type Object. " +
                                "Add a receiver argument with type Object resolve this." +
                                "If this method is not intended to be a library message then add the private or final modifier to ignore it.");
            } else {
                TypeMirror methodReceiverType = executable.getParameters().get(0).asType();
                if (inferredReceiverType == null) {
                    inferredReceiverType = methodReceiverType;
                } else if (!ElementUtils.isAssignable(methodReceiverType, inferredReceiverType)) {
                    if (!message.getName().equals("accepts")) {
                        message.addError(String.format("Invalid first argument type %s specified. " +
                                        "The first argument of a library method must be of the same type for all methods. " +
                                        "If this method is not intended to be a library message then add the private or final modifier to ignore it.",
                                        ElementUtils.getSimpleName(methodReceiverType)));
                    }
                }
            }

        }

        // deprecation handling + add messages to model
        messageByName: for (var entry : messageByName.entrySet()) {
            List<LibraryMessage> messages = entry.getValue();

            for (LibraryMessage message : messages) {
                if (message.isDeprecated() && message.isAbstract()) {
                    message.addError("A deprecated message must not be abstract. Deprecated messages need a default implementation.");
                    model.getMethods().add(message);
                    continue messageByName;
                }
            }

            if (messages.size() > 1) {
                List<LibraryMessage> deprecatedMessages = new ArrayList<>();
                List<LibraryMessage> nonDeprecatedMessages = new ArrayList<>();
                for (LibraryMessage overload : messages) {
                    if (overload.isDeprecated()) {
                        deprecatedMessages.add(overload);
                    } else {
                        nonDeprecatedMessages.add(overload);
                    }
                }
                LibraryMessage primaryMessage = null;
                if (nonDeprecatedMessages.size() > 1) {
                    for (LibraryMessage message : nonDeprecatedMessages) {
                        message.addError("Library message must have a unique name. Two methods with the same name found. " +
                                        "If this method is not intended to be a library message then add the private or final modifier to ignore it. " +
                                        "Note it is also possible to deprecate all messages except one using the @Deprecated annotation in order to evolve APIs in a compatible way.");
                    }
                    model.getMethods().addAll(nonDeprecatedMessages);
                    continue; // next group
                } else if (nonDeprecatedMessages.size() == 1) {
                    primaryMessage = nonDeprecatedMessages.get(0);

                    for (LibraryMessage message : deprecatedMessages) {
                        if (!primaryMessage.canBeDeprecatedFrom(message)) {
                            message.addError("Could not delegate from this deprecated message to method %s. Method parameters are not compatible.",
                                            ElementUtils.getReadableSignature(primaryMessage.getExecutable()));
                            model.getMethods().add(message);
                            continue messageByName;
                        }
                    }

                } else {
                    outer: for (LibraryMessage m1 : deprecatedMessages) {
                        for (LibraryMessage m2 : deprecatedMessages) {
                            if (!m1.canBeDeprecatedFrom(m2)) {
                                continue outer;
                            }
                        }
                        primaryMessage = m1;
                    }

                    if (primaryMessage == null) {
                        for (LibraryMessage message : deprecatedMessages) {
                            message.addError(
                                            "Could not determine primary overload for all deprecated messages. " +
                                                            "There must be one library message with the same name that all other messages can delegate to.");
                        }
                        model.getMethods().addAll(deprecatedMessages);
                        continue; // next group
                    }

                    deprecatedMessages.remove(primaryMessage);
                }
                for (LibraryMessage deprecated : deprecatedMessages) {
                    deprecated.setDeprecatedReplacement(primaryMessage);
                }

                primaryMessage.setDeprecatedOverloads(deprecatedMessages);
                model.getMethods().add(primaryMessage);
            } else {
                model.getMethods().addAll(messages);
            }
        }

        if (!model.hasErrors() && model.getMethods().size() <= 1) {
            model.addError("The library does not export any messages. Use public instance methods to declare library messages.");
        }

        // parse abstract methods
        Set<LibraryMessage> replacedMessages = new HashSet<>();
        for (LibraryMessage message : model.getMethods()) {
            AnnotationMirror abstractMirror = ElementUtils.findAnnotationMirror(message.getExecutable(), types.GenerateLibrary_Abstract);
            if (abstractMirror != null) {
                message.setAbstract(true);
                message.getAbstractIfExported().addAll(parseAbstractIfExported(message, abstractMirror, "ifExported", messageByName));
                message.getAbstractIfExportedAsWarning().addAll(parseAbstractIfExported(message, abstractMirror, "ifExportedAsWarning", messageByName));
                LibraryMessage replaced = parseAbstractReplacementFor(message, abstractMirror, "replacementOf", messageByName);
                ExecutableElement replacementMethod = parseAbstractReplacementWith(message, replaced, abstractMirror, "replacementMethod", allMethods);

                if (replacementMethod != null && replaced == null) {
                    message.addError("The 'replacementMethod' attribute is only valid when 'replacementOf' is also specified.");
                }
                if (replaced != null) {
                    if (!replacedMessages.add(replaced)) {
                        message.addError("Message " + message.getName() + " is a replacement of multiple messages. Arguments to replacementOf annotation have to be unique.");
                    }
                    message.setReplacementOf(replaced, replacementMethod);
                }
            }
        }

        if (inferredReceiverType == null) {
            inferredReceiverType = context.getType(Object.class);
        }

        if (!model.hasErrors() && inferredReceiverType.getKind().isPrimitive()) {
            model.addError("Primitive receiver type found. Only reference types are supported.");
            inferredReceiverType = context.getType(Object.class);
        }
        TypeMirror customReceiverType = ElementUtils.getAnnotationValue(TypeMirror.class, mirror, "receiverType", false);
        if (customReceiverType != null) {
            AnnotationValue customReceiverTypeValue = ElementUtils.getAnnotationValue(mirror, "receiverType");
            if (ElementUtils.typeEquals(customReceiverType, inferredReceiverType)) {
                model.addError(mirror, customReceiverTypeValue,
                                "Redundant receiver type. This receiver type could be inferred from the method signatures. Remove the explicit receiver type to resolve this redundancy.");
            }
            if (customReceiverType.getKind() != TypeKind.DECLARED) {
                model.addError(mirror, customReceiverTypeValue,
                                "Invalid type. Valid declared type expected.");
            }
            model.setExportsReceiverType(customReceiverType);
        } else {
            model.setExportsReceiverType(inferredReceiverType);
        }
        model.setSignatureReceiverType(inferredReceiverType);

        if (defaultExportReachable) {
            model.getDefaultExports().add(new LibraryDefaultExportData(null, context.getType(Object.class)));
            ExportsData exports = new ExportsData(context, type, mirror);
            ExportsLibrary objectExports = new ExportsLibrary(context, type, mirror, exports, model, model.getSignatureReceiverType(), true);

            for (LibraryMessage message : objectExports.getLibrary().getMethods()) {
                if (message.getName().equals("accepts")) {
                    continue;
                }
                ExportMessageData exportMessage = new ExportMessageData(objectExports, message, null, null);
                objectExports.getExportedMessages().put(message.getName(), exportMessage);
            }

            model.setObjectExports(objectExports);
        }

        for (LibraryMessage message : model.getMethods()) {
            // must be first to guarantee message lookup ordering
            model.getAllMethods().add(message);
            model.getAllMethods().addAll(message.getDeprecatedOverloads());
        }

        return model;
    }

    private static Set<LibraryMessage> parseAbstractIfExported(LibraryMessage message, AnnotationMirror abstractMirror, String ifExportedAttribute, Map<String, List<LibraryMessage>> messages) {
        Set<LibraryMessage> abstractIfExportedMessages = new LinkedHashSet<>();
        AnnotationValue value = ElementUtils.getAnnotationValue(abstractMirror, ifExportedAttribute);
        List<String> valueList = ElementUtils.getAnnotationValueList(String.class, abstractMirror, ifExportedAttribute);
        for (String ifExported : valueList) {
            List<LibraryMessage> ifExportedMessages = messages.get(ifExported);
            LibraryMessage ifExportedMessage = ifExportedMessages != null ? ifExportedMessages.get(0) : null;

            if (ifExportedMessage == message) {
                message.addError(abstractMirror, value, "The %s condition links to itself. Remove that condition to resolve this problem.", ifExportedAttribute);
            } else if (ifExportedMessage == null) {
                message.addError(abstractMirror, value, "The %s condition links to an unknown message '%s'. Only valid library messages may be linked.", ifExportedAttribute, ifExported);
            } else {
                abstractIfExportedMessages.add(ifExportedMessage);
            }
        }
        return abstractIfExportedMessages;
    }

    private static LibraryMessage parseAbstractReplacementFor(LibraryMessage message, AnnotationMirror abstractMirror, String replacementOfAttribute, Map<String, List<LibraryMessage>> messages) {
        String replacementValue = ElementUtils.getAnnotationValue(String.class, abstractMirror, replacementOfAttribute);
        if (replacementValue == null) {
            return null;
        }
        String replacement = replacementValue.replaceAll("\\s", "");
        if (replacement.isEmpty()) {
            return null;
        }
        int signatureStart = replacement.indexOf('(');
        if (signatureStart > 0) {
            String base = replacement.substring(0, signatureStart);
            List<LibraryMessage> list = messages.get(base);
            if (list != null) {
                ProcessorContext context = ProcessorContext.getInstance();
                String[] typeNames = replacement.substring(signatureStart + 1, replacement.length() - 1).split(",");
                List<TypeMirror> signature = new ArrayList<>(typeNames.length);
                for (String typeName : typeNames) {
                    TypeMirror parsedType = parseType(typeName, context);
                    signature.add(parsedType);
                    if (parsedType.getKind() == TypeKind.NONE) {
                        message.addError(abstractMirror, null, "Cannot parse replaced message parameter type %s. Use a qualified name for declared types.", typeName);
                    }
                }
                for (LibraryMessage msg : list) {
                    List<? extends TypeMirror> parameterTypes = ((ExecutableType) msg.getExecutable().asType()).getParameterTypes();
                    if (signature.size() == parameterTypes.size()) {
                        boolean sameSignature = true;
                        for (int i = 0; i < signature.size(); i++) {
                            sameSignature = ElementUtils.typeEquals(signature.get(i), parameterTypes.get(i));
                            if (!sameSignature) {
                                break;
                            }
                        }
                        if (sameSignature) {
                            validateAssignableArguments(message, abstractMirror, msg);
                            return msg;
                        }
                    }
                }
            }
        } else {
            List<LibraryMessage> list = messages.get(replacement);
            for (LibraryMessage msg : list) {
                if (replacement.equals(msg.getName())) {
                    validateAssignableArguments(message, abstractMirror, msg);
                    return msg;
                }
            }
        }
        message.addError(abstractMirror, null, "The replaced message %s was not found. Specify an existing message with optional type arguments.", replacementValue);
        return null;
    }

    private static TypeMirror parseType(String typeName, ProcessorContext context) {
        Types typeUtils = context.getEnvironment().getTypeUtils();
        // Primitives
        switch (typeName) {
            case "boolean":
                return typeUtils.getPrimitiveType(TypeKind.BOOLEAN);
            case "byte":
                return typeUtils.getPrimitiveType(TypeKind.BYTE);
            case "short":
                return typeUtils.getPrimitiveType(TypeKind.SHORT);
            case "int":
                return typeUtils.getPrimitiveType(TypeKind.INT);
            case "long":
                return typeUtils.getPrimitiveType(TypeKind.LONG);
            case "char":
                return typeUtils.getPrimitiveType(TypeKind.CHAR);
            case "float":
                return typeUtils.getPrimitiveType(TypeKind.FLOAT);
            case "double":
                return typeUtils.getPrimitiveType(TypeKind.DOUBLE);
            case "void":
                return typeUtils.getNoType(TypeKind.VOID);
        }
        // Arrays
        if (typeName.endsWith("[]")) {
            String elementType = typeName.substring(0, typeName.length() - 2);
            TypeMirror elemMirror = parseType(elementType, context);
            return typeUtils.getArrayType(elemMirror);
        }
        // Declared types with auto-import of java.lang.*
        Elements elementUtils = context.getEnvironment().getElementUtils();
        TypeElement el = elementUtils.getTypeElement(typeName);
        if (el == null && !typeName.contains(".")) {
            el = elementUtils.getTypeElement("java.lang." + typeName);
        }
        if (el != null) {
            return el.asType();
        }
        return typeUtils.getNoType(TypeKind.NONE);
    }

    private static void validateAssignableArguments(LibraryMessage message, AnnotationMirror abstractMirror, LibraryMessage msg) {
        ExecutableElement replacementMethodExecutable = message.getExecutable();
        List<? extends VariableElement> replacementMethodParameters = replacementMethodExecutable.getParameters();
        ExecutableElement replacedExecutable = msg.getExecutable();
        List<? extends VariableElement> replacedParameters = replacedExecutable.getParameters();
        int n = replacementMethodParameters.size();
        assert n == replacedParameters.size() : replacementMethodParameters.toString() + replacedParameters.toString();
        for (int i = 0; i < n; i++) {
            TypeMirror replaced = replacedParameters.get(i).asType();
            TypeMirror replacementMethod = replacementMethodParameters.get(i).asType();
            boolean isAssignable = ElementUtils.isAssignable(replaced, replacementMethod);
            if (!isAssignable) {
                String sign = ElementUtils.getReadableSignature(message.getExecutable());
                sign = sign.substring(sign.indexOf('('));
                String replacementMethodMessage = message.getName() + sign;
                sign = ElementUtils.getReadableSignature(msg.getExecutable());
                sign = sign.substring(sign.indexOf('('));
                String replacedMessage = msg.getName() + sign;
                message.addError(abstractMirror, null, "Type %s is not assignable from %s, message %s can not be replaced with %s.", replacementMethod, replaced, replacedMessage,
                                replacementMethodMessage);
            }
        }
    }

    private static ExecutableElement parseAbstractReplacementWith(LibraryMessage message, LibraryMessage replaced, AnnotationMirror abstractMirror, String replacementMethodAttribute,
                    List<ExecutableElement> allMethods) {
        String replacementMethodValue = ElementUtils.getAnnotationValue(String.class, abstractMirror, replacementMethodAttribute);
        if (replacementMethodValue != null) {
            String replacement = replacementMethodValue.replaceAll("\\s", "");
            if (!replacement.isEmpty()) {
                boolean hasWrongSignature = false;
                for (ExecutableElement executable : allMethods) {
                    String methodName = executable.getSimpleName().toString();
                    if (replacement.equals(methodName)) {
                        if (replaced == null || hasEqualSignature(replaced.getExecutable(), executable)) {
                            return executable;
                        } else {
                            hasWrongSignature = true;
                        }
                    }
                }
                if (hasWrongSignature) {
                    message.addError(abstractMirror, null, "The replacement method %s does not have signature and thrown types equal to the message %s it replaces.", replacement,
                                    ElementUtils.getReadableSignature(replaced.getExecutable()));
                } else {
                    message.addError(abstractMirror, null, "The replacement method %s does not exist.", replacement);
                }
            }
        }
        return null;
    }

    private static boolean hasEqualSignature(ExecutableElement executable1, ExecutableElement executable2) {
        if (!ElementUtils.typeEquals(executable1.getReturnType(), executable2.getReturnType())) {
            return false;
        }
        if (!ElementUtils.parameterTypesEquals(executable1, executable2)) {
            return false;
        }
        return ElementUtils.thrownTypesEquals(executable1, executable2);
    }

    private static void parseAssertions(Element element, AnnotationMirror mirror, TypeElement type, LibraryData model) {
        TypeMirror assertions = ElementUtils.getAnnotationValue(TypeMirror.class, mirror, "assertions", false);
        if (assertions != null) {
            AnnotationValue value = ElementUtils.getAnnotationValue(mirror, "assertions");
            TypeElement assertionsType = ElementUtils.castTypeElement(assertions);
            if (assertionsType.getModifiers().contains(Modifier.ABSTRACT)) {
                model.addError(value, "Assertions type must not be abstract.");
                return;
            }
            if (!ElementUtils.isVisible(element, assertionsType)) {
                model.addError(value, "Assertions type must be visible.");
                return;
            }
            if (!ElementUtils.isAssignable(assertions, type.asType())) {
                model.addError(value, "Assertions type must be a subclass of the library type '%s'.", ElementUtils.getSimpleName(model.getTemplateType()));
                return;
            }
            ExecutableElement foundConstructor = null;
            for (ExecutableElement constructor : ElementFilter.constructorsIn(assertionsType.getEnclosedElements())) {
                if (constructor.getParameters().size() == 1) {
                    if (ElementUtils.typeEquals(constructor.getParameters().get(0).asType(), model.getTemplateType().asType())) {
                        foundConstructor = constructor;
                        break;
                    }
                }
            }
            if (foundConstructor == null) {
                model.addError(value, "No constructor with single delegate parameter of type %s found.", ElementUtils.getSimpleName(model.getTemplateType()));
                return;
            }

            if (!ElementUtils.isVisible(model.getTemplateType(), foundConstructor)) {
                model.addError(value, "Assertions constructor is not visible.");
                return;
            }
            model.setAssertions(assertions);
        }
    }

    private LibraryDefaultExportData loadDefaultExportImpl(LibraryData model, AnnotationMirror exportAnnotation, String annotationName) {
        TypeMirror type = ElementUtils.getAnnotationValue(TypeMirror.class, exportAnnotation, annotationName);
        AnnotationValue typeValue = ElementUtils.getAnnotationValue(exportAnnotation, annotationName, false);
        if (typeValue == null) {
            return null;
        }
        if (type.getKind() != TypeKind.DECLARED) {
            model.addError(exportAnnotation, typeValue, "The %s type '%s' is invalid.", annotationName, ElementUtils.getSimpleName(type));
            return null;
        }

        List<AnnotationMirror> exportedLibraries = ElementUtils.getRepeatedAnnotation(ElementUtils.castTypeElement(type).getAnnotationMirrors(), types.ExportLibrary);
        TypeMirror receiverClass = null;
        for (AnnotationMirror exportedLibrary : exportedLibraries) {
            TypeMirror exportedLib = ElementUtils.getAnnotationValue(TypeMirror.class, exportedLibrary, "value");
            if (ElementUtils.typeEquals(model.getTemplateType().asType(), exportedLib)) {
                receiverClass = ElementUtils.getAnnotationValue(TypeMirror.class, exportedLibrary, "receiverType", false);
                if (receiverClass == null) {
                    model.addError(exportAnnotation, typeValue, "Default export '%s' must specify a receiverType.", ElementUtils.getSimpleName(exportedLib));
                    return null;
                }
                break;
            }
        }
        if (receiverClass == null) {
            model.addError(exportAnnotation, typeValue, "Default export '%s' does not export a library '%s'.", ElementUtils.getSimpleName(type),
                            ElementUtils.getSimpleName(model.getMessageElement().asType()));
            return null;
        }

        return new LibraryDefaultExportData(type, receiverClass);
    }

    @Override
    public DeclaredType getAnnotationType() {
        return types.GenerateLibrary;
    }

}
